package demoMod.stsconsole.map;

import demoMod.stsconsole.ConsoleEntry;
import demoMod.stsconsole.dungeons.AbstractDungeon;
import demoMod.stsconsole.helpers.MonsterHelper;
import demoMod.stsconsole.helpers.SaveHelper;
import demoMod.stsconsole.interfaces.ISubscriber;
import demoMod.stsconsole.interfaces.IgnoreMapNodeConnectionSubscriber;
import demoMod.stsconsole.rewards.AbstractReward;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class DungeonMap implements IgnoreMapNodeConnectionSubscriber {
    private final int width;
    private final int height;
    private final List<List<MapNode>> nodes;
    private final List<MapNode> availableNodes = new ArrayList<>();
    private boolean canGotoNode = false;

    public DungeonMap(int width, int height, int pathDensity, Random rng) {
        this.width = width;
        this.height = height;
        this.nodes = new ArrayList<>();
        createNodes();
        createPaths(pathDensity, rng);
        filterRedundantEdgesFromRow();
        ConsoleEntry.addLater.add(this);
    }

    public List<List<MapNode>> getNodes() {
        return nodes;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        int rowNum = this.nodes.size() - 1;
        int index = 0;
        availableNodes.clear();
        for (int leftPaddingSize = 5; rowNum >= 0; rowNum--) {
            sb.append("\n ").append(paddingGenerator(leftPaddingSize));
            String nodeSymbol;
            for (MapNode node : nodes.get(rowNum)) {
                String right = " ";
                String mid = " ";
                nodeSymbol = " ";
                for (MapEdge edge : node.getEdges()) {
                    if (edge.dstX < node.x) {
                        nodeSymbol = "\\";
                    }
                    if (edge.dstX == node.x) {
                        mid = "|";
                    }
                    if (edge.dstX > node.x) {
                        right = "/";
                    }
                }
                sb.append(nodeSymbol).append(mid).append(right);
            }
            sb.append("\n").append(rowNum).append(" ");
            sb.append(paddingGenerator(leftPaddingSize - String.valueOf(rowNum).length()));
            for (MapNode node : this.nodes.get(rowNum)) {
                nodeSymbol = " ";

                if (rowNum != this.nodes.size() - 1) {
                    if (node.hasEdges()) {
                        nodeSymbol = node.getRoomSymbol();
                    }
                } else {
                    for (MapNode lowerNode : nodes.get(rowNum - 1)) {
                        for (MapEdge edge : lowerNode.getEdges()) {
                            if (edge.dstX == node.x) {
                                nodeSymbol = node.getRoomSymbol();
                            }
                        }
                    }
                }
                ConsoleEntry.triggerSubscribers(IgnoreMapNodeConnectionSubscriber.class,
                        "canGotoThisNode",
                        new Class[]{MapNode.class},
                        new Object[]{node},
                        (method, params, subscriber) -> {
                            try {
                                if (!canGotoNode) canGotoNode = (boolean) method.invoke(subscriber, params);
                            } catch (IllegalAccessException | InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                );
                if (canGotoNode) {
                    sb.append(Integer.toString(index + 100).substring(1)).append(nodeSymbol);
                    index++;
                    availableNodes.add(node);
                } else {
                    if (node == AbstractDungeon.currDungeon.currMapNode) {
                        sb.append("->").append(nodeSymbol);
                    } else {
                        sb.append(" ").append(nodeSymbol).append(" ");
                    }
                }
                canGotoNode = false;
            }
        }
        sb.append("\nBoss: ").append(MonsterHelper.getEncounterNameById(AbstractDungeon.currDungeon.getBoss())).append("\n");
        return sb.toString();
    }

    public boolean gotoNode(int index) {
        MapNode prevNode = AbstractDungeon.currDungeon.currMapNode;
        if (AbstractDungeon.currDungeon.playerMapPosY >= this.nodes.size() - 1) {
            AbstractDungeon.currDungeon.currMapNode = getNode(0, AbstractDungeon.currDungeon.playerMapPosY + 1);
            if (!AbstractDungeon.currDungeon.currMapNode.prev.contains(prevNode)) {
                AbstractDungeon.currDungeon.currMapNode.prev.add(prevNode);
            }
        } else if (index >= 0 && index < availableNodes.size()) {
            AbstractDungeon.currDungeon.currMapNode = availableNodes.get(index);
        } else {
            return false;
        }
        ConsoleEntry.triggerSubscribers(IgnoreMapNodeConnectionSubscriber.class,
                "enterNewNode",
                new Class[]{MapNode.class},
                new Object[]{prevNode}
        );
        availableNodes.clear();
        SaveHelper.save();
        ConsoleEntry.triggerSubscribers(IgnoreMapNodeConnectionSubscriber.class,
                "enterNewNodePostSave",
                new Class[]{MapNode.class},
                new Object[]{prevNode}
        );
        return true;
    }

    private String paddingGenerator(int length) {
        StringBuilder str = new StringBuilder();
        for(int i = 0; i < length; ++i) {
            str.append(" ");
        }
        return str.toString();
    }

    private void createNodes() {
        for (int i=0;i<this.height;i++) {
            List<MapNode> row = new ArrayList<>();
            for (int j=0;j<this.width;j++) {
                MapNode node = new MapNode();
                node.x = j;
                node.y = i;
                row.add(node);
            }
            this.nodes.add(row);
        }
    }

    private void createPaths(int pathDensity, Random rng) {
        int firstRow = 0;
        int rowSize = this.nodes.get(firstRow).size() - 1;
        int firstStartingNode = -1;

        for (int i = 0; i < pathDensity; i++) {
            int startingNode = randRange(rng, 0, rowSize);
            if (i == 0) {
                firstStartingNode = startingNode;
            }

            while(startingNode == firstStartingNode && i == 1) {
                startingNode = randRange(rng, 0, rowSize);
            }

            _createPaths(new MapEdge(startingNode, -1, startingNode, 0), rng);
        }
    }

    private void filterRedundantEdgesFromRow() {
        List<MapEdge> existingEdges = new ArrayList();
        List<MapEdge> deleteList = new ArrayList();

        for (MapNode node : this.nodes.get(0)) {
            if (node.hasEdges()) {
                for (MapEdge edge : node.getEdges()) {
                    for (MapEdge prevEdge : existingEdges) {
                        if (edge.dstX == prevEdge.dstX && edge.dstY == prevEdge.dstY) {
                            deleteList.add(edge);
                        }
                    }
                    existingEdges.add(edge);
                }

                for (MapEdge edge : deleteList) {
                    node.delEdge(edge);
                    MapNode nodeToDisconnect = getNode(edge.dstX, edge.dstY);
                    if (nodeToDisconnect.prev.contains(node) && node.next.contains(nodeToDisconnect)) {
                        nodeToDisconnect.prev.remove(node);
                        node.next.remove(nodeToDisconnect);
                    }
                }
                deleteList.clear();
            }
        }
    }

    private void _createPaths(MapEdge edge, Random rng) {
        MapNode currNode = getNode(edge.dstX, edge.dstY);
        if (edge.dstY + 1 < this.nodes.size()) { //如果不是boss房间的前一层
            int rowWidth = nodes.get(edge.dstY).size();
            int rowEndNode = rowWidth - 1;
            byte min;
            byte max;
            if (edge.dstX == 0) {
                min = 0;
                max = 1;
            } else if (edge.dstX == rowEndNode) {
                min = -1;
                max = 0;
            } else {
                min = -1;
                max = 1;
            }

            int newEdgeX = edge.dstX + randRange(rng, min, max);
            int newEdgeY = edge.dstY + 1;
            MapNode targetNodeCandidate = getNode(newEdgeX, newEdgeY);
            int minAncestorGap = 3;
            int maxAncestorGap = 5;
            List<MapNode> parents = targetNodeCandidate.prev;
            if (!parents.isEmpty()) {
                for (MapNode parent : parents) {
                    if (parent != currNode) {
                        MapNode ancestor = getCommonAncestor(parent, currNode, maxAncestorGap);
                        if (ancestor != null) {
                            int ancestor_gap = newEdgeY - ancestor.y;
                            if (ancestor_gap < minAncestorGap) {
                                if (targetNodeCandidate.x > currNode.x) {
                                    newEdgeX = edge.dstX + randRange(rng, -1, 0);
                                    if (newEdgeX < 0) {
                                        newEdgeX = edge.dstX;
                                    }
                                } else if (targetNodeCandidate.x == currNode.x) {
                                    newEdgeX = edge.dstX + randRange(rng, -1, 1);
                                    if (newEdgeX > rowEndNode) {
                                        newEdgeX = edge.dstX - 1;
                                    } else if (newEdgeX < 0) {
                                        newEdgeX = edge.dstX + 1;
                                    }
                                } else {
                                    newEdgeX = edge.dstX + randRange(rng, 0, 1);
                                    if (newEdgeX > rowEndNode) {
                                        newEdgeX = edge.dstX;
                                    }
                                }

                                targetNodeCandidate = getNode(newEdgeX, newEdgeY);
                            }
                        }
                    }
                }
            }

            MapNode rightNode;
            MapEdge leftEdgeOfRightNode;

            if (edge.dstX != 0) {
                rightNode = nodes.get(edge.dstY).get(edge.dstX - 1);
                if (rightNode.hasEdges()) {
                    leftEdgeOfRightNode = getMaxEdge(rightNode.getEdges());
                    if (leftEdgeOfRightNode.dstX > newEdgeX) {
                        newEdgeX = leftEdgeOfRightNode.dstX;
                    }
                }
            }

            if (edge.dstX < rowEndNode) {
                rightNode = nodes.get(edge.dstY).get(edge.dstX + 1);
                if (rightNode.hasEdges()) {
                    leftEdgeOfRightNode = getMinEdge(rightNode.getEdges());
                    if (leftEdgeOfRightNode.dstX < newEdgeX) {
                        newEdgeX = leftEdgeOfRightNode.dstX;
                    }
                }
            }

            targetNodeCandidate = getNode(newEdgeX, newEdgeY);
            MapEdge newEdge = new MapEdge(edge.dstX, edge.dstY, newEdgeX, newEdgeY);
            currNode.addEdge(newEdge);
            currNode.getEdges().sort(MapEdge::compareTo);
            targetNodeCandidate.prev.add(currNode);
            currNode.next.add(targetNodeCandidate);
            _createPaths(newEdge, rng);
        }
    }

    private MapEdge getMaxEdge(List<MapEdge> edges) {
        edges.sort(MapEdge::compareTo);

        assert !edges.isEmpty() : "Somehow the edges are empty. This shouldn't happen.";

        return edges.get(edges.size() - 1);
    }

    private MapEdge getMinEdge(List<MapEdge> edges) {
        edges.sort(MapEdge::compareTo);

        assert !edges.isEmpty() : "Somehow the edges are empty. This shouldn't happen.";

        return edges.get(0);
    }

    private MapNode getCommonAncestor(MapNode node1, MapNode node2, int max_depth) {
        assert node1.y == node2.y;

        assert node1 != node2;

        MapNode l_node;
        MapNode r_node;
        if (node1.x < node2.y) {
            l_node = node1;
            r_node = node2;
        } else {
            l_node = node2;
            r_node = node1;
        }

        int current_y = node1.y;

        while(true) {
            if (current_y >= 0 && current_y >= node1.y - max_depth) {
                if (!l_node.prev.isEmpty() && !r_node.prev.isEmpty()) {
                    l_node = getNodeWithMaxX(l_node.prev);
                    r_node = getNodeWithMinX(r_node.prev);
                    if (l_node == r_node) {
                        return l_node;
                    }
                    --current_y;
                    continue;
                }
                return null;
            }
            return null;
        }
    }

    private MapNode getNodeWithMaxX(List<MapNode> nodes) {
        assert !nodes.isEmpty() : "The nodes are empty, this shouldn't happen.";

        MapNode max = nodes.get(0);

        for (MapNode node : nodes) {
            if (node.x > max.x) {
                max = node;
            }
        }

        return max;
    }

    private MapNode getNodeWithMinX(List<MapNode> nodes) {
        assert !nodes.isEmpty() : "The nodes are empty, this shouldn't happen.";

        MapNode min = nodes.get(0);

        for (MapNode node : nodes) {
            if (node.x < min.x) {
                min = node;
            }
        }

        return min;
    }

    public MapNode getNode(int x, int y) {
        if (y >= this.nodes.size()) {
            return AbstractDungeon.currDungeon.getSpecialMapNode(y);
        }
        return this.nodes.get(y).get(x);
    }

    private int randRange(Random rng, int min, int max) {
        return rng.nextInt(max - min + 1) + min;
    }

    @Override
    public boolean enabled(Class<? extends ISubscriber> cls) {
        boolean result = AbstractDungeon.currDungeon != null && AbstractDungeon.currDungeon.map == this;
        if (!result) {
            ConsoleEntry.removeLater.add(this);
        }
        return result;
    }

    @Override
    public boolean canGotoThisNode(MapNode node) {
        return node.prev.contains(AbstractDungeon.currDungeon.currMapNode);
    }

    @Override
    public void enterNewNode(MapNode prevNode) {
        for (AbstractReward reward : prevNode.room.rewards) {
            reward.skipped();
        }
        AbstractDungeon.floorNum++;
        AbstractDungeon.currDungeon.playerMapPosY = AbstractDungeon.currDungeon.currMapNode.y;
        AbstractDungeon.currDungeon.playerMapPosX = AbstractDungeon.currDungeon.currMapNode.x;
    }

    @Override
    public void enterNewNodePostSave(MapNode prevNode) {
        AbstractDungeon.currDungeon.currMapNode.room.onPlayerEntry();
        AbstractDungeon.currDungeon.increaseEventPoolIndex();
    }
}
